package com.zktravel.core.bean;

import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;

import com.google.common.base.Objects;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.odianyun.util.db.mybatis.dao.EntityQueryParam;
import com.odianyun.util.db.query.PageQueryParam;
import com.odianyun.util.db.query.QueryParam;
import com.odianyun.util.db.query.Sort;
import com.odianyun.util.value.ValueUtils;

/**
 * QueryParam转换器, 默认根据Key来确定查询操作符：
 * <p>如果是以Name或name结尾，则为like查询</p>
 * <p>如果是startXXXTime、startXXXDate、beginXXXTime、beginXXXDate其中一种，则转换为java.util.Date类型，并且为大于等于查询</p>
 * <p>如果是endXXXTime或endXXXDate，则转换为java.util.Date类型，并且为小于等于查询</p>
 */
public class QueryParamConvertor implements Cloneable {
    private final static int MAX_IN_SIZE = 2000;
	private final static String[] DATE_GTE_BEGIN_KEYS = new String[] {"start", "begin"};
	private final static String[] DATE_LTE_BEGIN_KEYS = new String[] {"end"};
	private final static String[] DATE_END_KEYS = new String[] {"time", "Time", "date", "Date"};
	private final static String[] LIKE_END_KEYS = new String[] {"name", "Name"};

	private boolean skipSorts;
	
	private List<String> includeKeys;
	
	private Set<String> ignoreKeys = Sets.newHashSet();
	
	private QueryArgs args;
	private String[] dateEndKeys = DATE_END_KEYS;
	private String[] rightLikeEndKeys = LIKE_END_KEYS;
	private String[] likeKeys;
	private String[] leftLikeKeys;
	private String[] rightLikeKeys;
	private boolean emptyFilterToNull;
	private Map<String, String> renameMap = Maps.newHashMapWithExpectedSize(2);
	
	public QueryParamConvertor(QueryArgs args) {
		this.args = args;
	}
	
	public String[] getRightLikeEndKeys() {
		return rightLikeEndKeys;
	}
	public QueryParamConvertor withRightLikeEndKeys(String... rightLikeEndKeys) {
		this.rightLikeEndKeys = rightLikeEndKeys;
    	return this;
	}
	public QueryParamConvertor withLikeKeys(String... likeKeys) {
		this.likeKeys = likeKeys;
		return this;
	}
	public QueryParamConvertor withLeftLikeKeys(String... leftLikeKeys) {
		this.leftLikeKeys = leftLikeKeys;
		return this;
	}
	public QueryParamConvertor withRightLikeKeys(String... rightLikeKeys) {
		this.rightLikeKeys = rightLikeKeys;
		return this;
	}
	public String[] getDateEndKeys() {
		return dateEndKeys;
	}
	public QueryParamConvertor withDateEndKeys(String... dateEndKeys) {
		this.dateEndKeys = dateEndKeys;
    	return this;
	}
	
	public QueryParamConvertor withSkipSorts() {
		this.skipSorts = true;
		return this;
	}
	
	public QueryParamConvertor withEmptyFilterToNull() {
		this.emptyFilterToNull = true;
		return this;
	}
	
	public QueryParamConvertor withIncludeKeys(String... keys) {
		this.includeKeys = Arrays.asList(keys);
		return this;
	}
	
	public QueryParamConvertor rename(String key, String changeTo) {
		renameMap.put(key, changeTo);
		return this;
	}
	
	public EntityQueryParam toEntityQueryParam(Class<?> entityClass) {
		return toEntityQueryParam(entityClass, null);
	}
	
	public EntityQueryParam toEntityQueryParam(Class<?> entityClass, String prefix) {
		QueryParam param = toQueryParam();
		if (param == null) {
			return null;
		}
		return new EntityQueryParam(entityClass, prefix).fromFilterParam(param);
	}
	
    public QueryParam toQueryParam() {
    	QueryParam queryParam = null;
    	
    	if (args instanceof PageQueryArgs) {
    		PageQueryParam pageQueryParam = new PageQueryParam();
    		PageQueryArgs pageQueryArgs = (PageQueryArgs) args;
    		
    		pageQueryParam.withPage(pageQueryArgs.getPage());
    		pageQueryParam.withLimit(pageQueryArgs.getLimit());
    		
    		queryParam = pageQueryParam;
    	} else {
    		queryParam = new QueryParam();
    	}
    	if (! skipSorts) {
        	setSortsToQueryParam(queryParam);
    	}
    	
    	setFiltersToQueryParam(queryParam);
    	
    	if (emptyFilterToNull && CollectionUtils.isEmpty(queryParam.getFilters())) {
    		return null;
    	}
    	
    	return queryParam;
    }
    
    private void setSortsToQueryParam(QueryParam queryParam) {
    	List<Sort> sorts = args.getSorts();
    	if (sorts != null) {
    		for (Sort sort : sorts) {
    			String name = getName(sort.getField());
    			if (sort.isAsc()) {
    				queryParam.asc(name);
    			} else {
					queryParam.desc(name);
				}
    		}
    	}
    }
    
    private void setFiltersToQueryParam(QueryParam queryParam) {
    	Map<String, Object> filters = args.getFilters();
    	if (filters.size() > 0) {
    		Set<String> keys = filters.keySet();
    		if (includeKeys != null) {
    			keys = new HashSet<String>(includeKeys);
    		}
    		
    		for (String key : keys) {
    			if (ignoreKeys.contains(key)) {
    				continue;
    			}
    			
    			String name = getName(key);
    			Object value = filters.get(key);
    			if (value == null || Objects.equal(value, "")) continue;
    			
    			if (value.getClass().isArray()) {
    				Object[] array = (Object[]) value;
    				if (array.length > 0) {
                        Assert.isTrue(array.length <= MAX_IN_SIZE, "Array length must be less than or equal to " + MAX_IN_SIZE);
                        queryParam.in(name, array);
    				}
        			continue;
    			}
    			if (Collection.class.isAssignableFrom(value.getClass())) {
    				Collection<?> coll = (Collection<?>) value;
    				if (coll.size() > 0) {
                        Assert.isTrue(coll.size() <= MAX_IN_SIZE, "Collection size must be less than or equal to " + MAX_IN_SIZE);
            			queryParam.in(name, new HashSet<>(coll).toArray());
    				}
        			continue;
    			}
    			
    			if (containsKey(key, rightLikeKeys)) {
    				queryParam.liker(name, args.get(key, String.class));
    				continue;
    			}
    			
    			if (containsKey(key, leftLikeKeys)) {
    				queryParam.likel(name, args.get(key, String.class));
    				continue;
    			}
    			
    			if (containsKey(key,likeKeys)) {
    				queryParam.like(name, args.get(key, String.class));
    				continue;
    			}
    			
    			if (endsWithAny(key, getRightLikeEndKeys())) {
    				queryParam.liker(name, args.get(key, String.class));
    				continue;
    			}
    			
    			if (endsWithAny(key, getDateEndKeys())) {
        			String dateGteKey = getDateGteKey(name);
        			if (dateGteKey != null) {
        				queryParam.gte(dateGteKey, getDateGteValue(args.get(key, Date.class)));
        				continue;
        			}

        			String dateLteKey = getDateLteKey(name);
        			if (dateLteKey != null) {
        				queryParam.lte(dateLteKey, getDateLteValue(args.get(key, Date.class)));
        				continue;
        			}
        			
    				queryParam.eq(name, args.get(key, Date.class));
    				continue;
    			}
    			
    			queryParam.eq(name, filters.get(key));
    		}
    	}
    }
    
    public QueryParamConvertor ignore(String... key) {
    	for (String k : key) {
    		ignoreKeys.add(k);
    	}
    	return this;
    }
    
    private String getName(String key) {
    	return ValueUtils.ifNull(renameMap.get(key), key);
    }
    
    private boolean endsWithAny(String key, String[] array) {
    	if (array == null) return false;
    	for (String s : array) {
    		if (key.endsWith(s)) return true;
    	}
    	return false;
    }

    private boolean containsKey(String key, String[] array) {
    	if (array == null) return false;
    	for (String s : array) {
    		if (s.equals(key)) return true;
    	}
    	return false;
    }
    
    private String getDateGteKey(String key) {
    	for (String s : DATE_GTE_BEGIN_KEYS) {
    		if (key.startsWith(s)) {
    			return StringUtils.uncapitalize(key.substring(s.length()));
    		}
    	}
    	return null;
    }
    
    private String getDateLteKey(String key) {
    	for (String s : DATE_LTE_BEGIN_KEYS) {
    		if (key.startsWith(s)) {
    			return StringUtils.uncapitalize(key.substring(s.length()));
    		}
    	}
    	return null;
    }
    
    private Date getDateGteValue(Date value) {
    	if (value == null) return null;
    	return getDayBegin(value);
    }
    
    private Date getDateLteValue(Date value) {
    	if (value == null) return null;
    	return getDayEnd(value);
    }
    
    private Date getDayBegin(Date date) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		
		if (hasTime(cal)) {
			return date;
		}
		
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		
		return cal.getTime();
    }
    
    private Date getDayEnd(Date date) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		
		if (hasTime(cal)) {
			return date;
		}
		
		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);
		cal.set(Calendar.MILLISECOND, 999);
		
		return cal.getTime();
    }
    
    private boolean hasTime(Calendar cal) {
    	return cal.get(Calendar.HOUR) != 0 || cal.get(Calendar.MINUTE) != 0 || cal.get(Calendar.SECOND) != 0;
    }

	@Override
	public QueryParamConvertor clone() {
		try {
			QueryParamConvertor clone = (QueryParamConvertor) super.clone();
			if (clone.ignoreKeys != null) clone.ignoreKeys.clear();
			if (clone.includeKeys != null) clone.includeKeys.clear();
			if (clone.renameMap != null) clone.renameMap.clear();
			return clone;
		} catch (CloneNotSupportedException e) {
			return null;
		}
	}
    
}

